package com.lizardtech.djvu
{
	import com.lizardtech.djvu.codecs.*;
	
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.events.TimerEvent;
	import flash.utils.ByteArray;
	import flash.utils.Timer;
	
	import java.util.Vector;
	import java.util.gc;
	
	public class DjVuDecoder
	{
		
		private var chunks:Array = new Array();
		private var djvufile:ByteArray;
		private var targetSprite:Sprite;
		private var djInfo:djINFO;
		private var BgIWPixmap:IWPixmap = new IWPixmap();
		private var fgIWPixmap:IWPixmap = new IWPixmap();
		private var fgPixmap:GPixmap = new GPixmap();
		private var FgJb2:JB2Image = new JB2Image();
		private var FgPalette:Palette = new Palette();
		private var renderedImage:BitmapData;
		
		private var asyncTimer:Timer = new Timer(250);
		private var asyncMode:uint = 0;
		private var ASYNC_MODE_DECODE_CHUNKS:uint = 1;
		private var ASYNC_MODE_RENDER:uint = 2;
		private var asyncState:Object = new Object();
		
		public function DjVuDecoder() {
			asyncTimer.addEventListener(TimerEvent.TIMER, onAsyncTick);
		}
		
		public function decode(file:ByteArray=null):void {
			if(file) { this.djvufile = file; }
			getChunks();
			
			// iterate thru each chunk
			for each(var chunk:IFFChunk in chunks) {
				//decodeChunk(chunk)
				
				// decode each chunk asycnhronously
				trace("Enabling asynch mode.");
				asyncMode = ASYNC_MODE_DECODE_CHUNKS;
				asyncState.chunk = 0;
				asyncState.ready = true;
				asyncTimer.start();
			}
			
			
			//
			
		}
		
		private function render():void {
			var rectWidth:int = 100;
			var rectHeight:int = 100;
			var offsetX:int = 200;
			var offsetY:int = 200;
			
			trace("beginning render...");
			
			
			
			renderedImage = new BitmapData(rectWidth, rectHeight, false);
			
			
			var subsample:int = com.lizardtech.djvu.DjVuPage.compute_red(rectWidth, rectHeight, rectWidth, rectHeight);
			
			var GPMap:GMap = getPixmap(
					//new GRect(offsetX, offsetY + djInfo.height - rectHeight, rectWidth, rectHeight),
					new GRect(0, 0, rectWidth, rectHeight),
					subsample, 
					2.2, 
					null);
			
			if(GPMap == null) { // this must be a bitonal image
				GPMap = getBitmap(
						new GRect(offsetX, offsetY + djInfo.height - rectHeight, rectWidth, rectHeight),
						subsample, 
						1, 
						null);
			}
			
			
			var xPos:uint = 0;
			var yPos:uint = 0;
			
			// loop through each pixel, align bits, translate then copy to bitmap model
			for(var pos:uint = 0; pos < (rectWidth*rectHeight); pos++) {
				var currPixel:GPixel;
				if(GPMap.isRampNeeded()) { // djvu files encoded at less than 24 bit
					currPixel = GPMap.ramp(new GPixelReference(GPMap, pos));
				} else { // already 24 bit color images
					currPixel = new GPixelReference(GPMap, pos);
				}
				
				try {
					var rgb:uint = currPixel.getRed()*256*256 + currPixel.getGreen()*256 + currPixel.getBlue();
					//renderedImage.setPixel(xPos, djInfo.height - yPos, rgb); 
					renderedImage.setPixel(xPos, yPos, rgb);
					//trace(xPos + "," +  yPos + ": " + currPixel.getRed() + "." + currPixel.getGreen() + "." + currPixel.getBlue());
				} catch (e:*) { // for some reason this occasionally fails. replace faulty pixel with a white pixel
					// do nothing- default pixel is already white
					//iBufferedImage.setRGB(xPos, height - yPos, 0xffffff);
				}	
				
				// track our x and y position
				xPos++;
				if(xPos == rectWidth) {
					xPos = 0;
					yPos++;
				}
			}
			
			var rBitmap:Bitmap = new Bitmap(renderedImage);
			targetSprite.addChild(rBitmap);
			
			//var imageSprite:Sprite = new Sprite();
			//imageSprite.ad
			
			trace("finished");
		}
		
		public function onAsyncTick(event:TimerEvent):void {
			if(asyncMode == ASYNC_MODE_DECODE_CHUNKS) {
				if(asyncState.ready) {
					asyncState.chunk++;
					asyncState.ready = false;
					var nextChunk:IFFChunk = chunks[asyncState.chunk];
					if((nextChunk) && (asyncState.chunk < 20)) {  
						decodeChunk(nextChunk);
					} else { 
						
						asyncMode = ASYNC_MODE_RENDER;
					}	
					asyncState.ready = true;
				}
			}
			if(asyncMode == ASYNC_MODE_RENDER) {
				render();
				asyncMode = 0; // idle
			}
		}

		private function decodeChunk(chunk:IFFChunk):void {
			var decoder:djCodec = null;
				
			// configure decoders here
			if(chunk.name == "INFO") { 
				decoder = new djINFO();
				decoder.decode(chunk);
				djInfo = decoder as djINFO;
			} else if(chunk.name == "BG44") {
				//decoder = new djBG44();
				BgIWPixmap.decodeChunk(chunk);
			}
		}

		private function getChunks():void {
			djvufile.position = 0;
			
			// check for DJVU file identifier
			var checkMagic:String = djvufile.readUTFBytes(4);
			if(checkMagic != "AT&T") { throw Error("Not a DJVU File."); }
			
			while(djvufile.bytesAvailable>0) {
				var chunkName:String = djvufile.readUTFBytes(4);
				var chunkSize:uint;
				
				// sometimes we get behind by a byte. not sure why..
				if(chunkName.length == 0) {
					djvufile.position -= 3;
					continue;
				}
				
				
				// DJVU isn't really a separate chunk...
				if(chunkName != "DJVU") { chunkSize = djvufile.readInt(); }
				
				trace("Found Chunk: [" + chunkName + "] "+chunkSize+" (" +djvufile.position+ ", " + (djvufile.position + chunkSize) + ")"); 
				var chunk:IFFChunk = new IFFChunk(djvufile, chunkName, djvufile.position, (djvufile.position + chunkSize));
				chunks.push(chunk);
				if((chunkName != "FORM") && (chunkName != "DJVU")) {
					djvufile.position += chunkSize;
				}
			}
		}
		
		
		   /**
		   * Get the pixmap for the desired region.
		   *
		   * @param rect area of interest
		   * @param subsample rate to subsample
		   * @param gamma color correction factor
		   * @param retval an old image to use instead of creating a new one
		   *
		   * @return the pixmap of interest
		   */
		  public function getPixmap(
		    rect:GRect,
		    subsample:int,
		    gamma:Number,
		    retval:GPixmap):GPixmap{
		    if(rect.isEmpty())
		    {
		      return (retval == null)
		      ? (new GPixmap())
		      : retval.init(0, 0, null);
		    }
		
		    var bg:GPixmap=getBgPixmap(rect, subsample, gamma, retval);
		    if(true) {
		      if(bg == null)
		      {
		          bg=(retval == null)?new GPixmap():retval;
		          bg.init(
		           rect.height(), 
		           rect.width(), GPixel.WHITE);
		      } 
		      if(stencil(bg, rect, subsample, gamma)) {
		        retval=bg;
		      }
		      
		      retval = bg;
		   
		    }
		    return retval;
		  }
		  
		  /**
		   * Get the background pixmap.
		   *
		   * @param rect area to render
		   * @param subsample rate to subsample
		   * @param gamma color correction factor
		   * @param retval an old pixmap to reuse rather than allocating more memory
		   *
		   * @return the pixmap of interest
		   */
		  public function getBgPixmap(
		    rect:GRect,
		    subsample:int,
		    gamma:Number,
		    retval:GPixmap):GPixmap{
		    var pm:GPixmap= null;
		    var info:djINFO = djInfo;
		    var width:int= (info == null)
		      ? 0: info.width;
		    var height:int= (info == null)
		      ? 0: info.height;
		
		    if((width <= 0) || (height <= 0) || (info == null))
		    {
		      return null;
		    }
		
		    var gamma_correction:Number= 1.0;
		
		    if((gamma > 0) && (info != null))
		    {
		      gamma_correction = gamma / info.gamma;
		    }
		
		    if(gamma_correction < 0.1)
		    {
		      gamma_correction = 0.1;
		    }
		    else if(gamma_correction > 10)
		    {
		      gamma_correction = 10;
		    }
		
		    var bgIWPixmap:IWPixmap= BgIWPixmap;
		
		    if(bgIWPixmap != null)
		    {
		      var w:int= bgIWPixmap.getWidth();
		      var h:int= bgIWPixmap.getHeight();
		
		      if((w == 0) || (h == 0) || (width == 0) || (height == 0))
		      {
		        return null;
		      }
		
		      var red:int= compute_red(width, height, w, h);
		
		      if((red < 1) || (red > 12))
		      {
		        return null;
		      }
		
		      if(subsample == red)
		      {
		        pm = bgIWPixmap.getPixmap(1, rect, retval);
		      }
		      else if(subsample == (2* red))
		      {
		        pm = bgIWPixmap.getPixmap(2, rect, retval);
		      }
		      else if(subsample == (4* red))
		      {
		        pm = bgIWPixmap.getPixmap(4, rect, retval);
		      }
		      else if(subsample == (8* red))
		      {
		        pm = bgIWPixmap.getPixmap(8, rect, retval);
		      }
		      else if((red * 4) == (subsample * 3))
		      {
		        var xrect:GRect= new GRect();
		        xrect.xmin   = int(Math.floor(rect.xmin*4/3));
		        xrect.ymin   = int(Math.floor(rect.ymin*4/3));
		        xrect.xmax   = int(Math.ceil(rect.xmax*4/3));
		        xrect.ymax   = int(Math.ceil(rect.ymax*4/3));
		        var nrect:GRect=new GRect(0,0, rect.width(), rect.height());
		        if(xrect.xmax > w)
		        {
		          xrect.xmax = w;
		        }
		
		        if(xrect.ymax > h)
		        {
		          xrect.ymax = h;
		        }
		
		        var ipm:GPixmap= bgIWPixmap.getPixmap(1, xrect, null);
		        pm = (retval != null) 
		          ? retval
		          : new GPixmap();
		        pm.downsample43(ipm, nrect);
		      }
		      else
		      {
		        var po2:int= 16;
		
		        while((po2 > 1) && (subsample < (po2 * red)))
		        {
		          po2 >>= 1;
		        }
		
		        var inw:int= ((w + po2) - 1) / po2;
		        var inh:int= ((h + po2) - 1) / po2;
		        var outw:int= ((width + subsample) - 1) / subsample;
		        var outh:int= ((height + subsample) - 1) / subsample;
		        var ps:GPixmapScaler= createGPixmapScaler(inw, inh, outw, outh);
		        ps.setHorzRatio(red * po2, subsample);
		        ps.setVertRatio(red * po2, subsample);
		
		        var xrect:GRect= ps.getRequiredRect(rect);
		        var ipm:GPixmap= bgIWPixmap.getPixmap(po2, xrect, null);
		        pm = (retval != null)
		          ? retval
		          : new GPixmap();
		        ps.scale(xrect, ipm, rect, pm);
		      }
		
		      if((pm != null) && (gamma_correction != 1.0))
		      {
		        pm.applyGammaCorrection(gamma_correction);
		
		        for(var i:int= 0; i < 9; i++)
		        {
		          pm.applyGammaCorrection(gamma_correction);
		        }
		      }
		
		      if(DjVuOptions.COLLECT_GARBAGE)
		      {
		 		java.util.gc.forceGC();
		      }
		
		      return pm;
		    }
		    else
		    {
		      return null;
		    }
  		}
  		
  		  public function createGPixmapScaler(
		    inw:int,
		    inh:int,
		    outw:int,
		    outh:int):GPixmapScaler{
		    return new GPixmapScaler(inw, inh, outw, outh);
		  }
		  
		  
		  /**
		   * Create an image by stenciling the foreground onto the background.
		   *
		   * @param pm the background image to stencil
		   * @param rect the area of the page being stenciled
		   * @param subsample the subsample rate of the area being stenciled
		   * @param gamma the color correction factor
		   *
		   * @return true if successfull
		   */
		  public function stencil(
		    pm:GPixmap,
		    rect:GRect,
		    subsample:int,
		    gamma:Number):Boolean{
		    var info:djINFO = djInfo;
		
		    if(info == null) { return false; }
		
		    var width:int= info.width;
		    var height:int= info.height;
		
		    if((width <= 0) || (height <= 0)) { 
		    	return false; 
		    }
		
		    var gamma_correction:Number= 1.0;
		
		    if(gamma > 0.0)
		    {
		      gamma_correction = gamma / info.gamma;
		    }
		
		    if(gamma_correction < 0.10000000000000001)
		    {
		      gamma_correction = 0.10000000000000001;
		    }
		    else if(gamma_correction > 10)
		    {
		      gamma_correction = 10;
		    }
		
		    var fgJb2:JB2Image= fgJb2;
		
		    if(fgJb2 != null)
		    {
		      var fgPalette:Palette= FgPalette;
		
		      if(fgPalette != null)
		      {
		        var components:Vector = new Vector();
		        var bm:GBitmap= get_bitmap(rect, subsample, 1, components);
		
		        if(fgJb2.get_blit_count() != fgPalette.colordata.length)
		        {
		          pm.attenuate(bm, 0, 0);
		
		          return false;
		        }
		
		        var colors:GPixmap= new GPixmap();
		         	colors.init(
		            1,
		            fgPalette.size(),
		            null);
		        var color:GPixelReference= colors.createGPixelReference(0);
		
		        for(var i:int= 0; i < colors.columns(); color.incOffset())
		        {
		          fgPalette.index_to_color(i++, color);
		        }
		
		        colors.applyGammaCorrection(gamma_correction);
		
		        var compset:Vector= new Vector();
		
		        while(components.size() > 0)
		        {
		          var lastx:int= 0;
		          var colorindex:int=
		            fgPalette.colordata[int(components.elementAt(0))];
		          var comprect:GRect= new GRect();
		          compset.setSize(0);
		
		          for(var pos:int= 0; pos < components.size();)
		          {
		            var blitno:int=
		              int(components.elementAt(pos));
		            var pblit:JB2Blit= fgJb2.get_blit(blitno);
		
		            if(pblit.ileft < lastx)
		            {
		              break;
		            }
		
		            lastx = pblit.ileft;
		
		            if(fgPalette.colordata[blitno] == colorindex)
		            {
		              var pshape:JB2Shape= fgJb2.get_shape(pblit.ishapeno);
		              var xrect:GRect=
		                new GRect(
		                  pblit.ileft,
		                  pblit.ibottom,
		                  pshape.getGBitmap().columns(),
		                  pshape.getGBitmap().rows());
		              comprect.recthull(comprect, xrect);
		              compset.addElement(components.elementAt(pos));
		              components.removeElementAt(pos);
		            }
		            else
		            {
		              pos++;
		            }
		          }
		
		          comprect.xmin /= subsample;
		          comprect.ymin /= subsample;
		          comprect.xmax   = ((comprect.xmax + subsample) - 1) / subsample;
		          comprect.ymax   = ((comprect.ymax + subsample) - 1) / subsample;
		          comprect.intersect(comprect, rect);
		
		          if(comprect.isEmpty())
		          {
		            continue;
		          }
		
		          //        bm   = getBitmap(comprect, subsample, 1);
		          bm = new GBitmap();
		          bm.init(
		            comprect.height(),
		            comprect.width(),
		            0);
		          bm.setGrays(1+ (subsample * subsample));
		
		          var rxmin:int= comprect.xmin * subsample;
		          var rymin:int= comprect.ymin * subsample;
		
		          for(var pos:int= 0; pos < compset.size(); ++pos)
		          {
		            var blitno:int=
		              int(compset.elementAt(pos));
		            var pblit:JB2Blit= fgJb2.get_blit(blitno);
		            var pshape:JB2Shape= fgJb2.get_shape(pblit.ishapeno);
		            bm.blit(
		              pshape.getGBitmap(),
		              pblit.ileft - rxmin,
		              pblit.ibottom - rymin,
		              subsample);
		          }
		
		          color.setOffset(colorindex);
		          pm.blit(
		            bm,
		            comprect.xmin - rect.xmin,
		            comprect.ymin - rect.ymin,
		            color);
		        }
		
		        return true;
		      }
		
		      // Three layer model.
		      var fgIWPixmap:IWPixmap= fgIWPixmap;
		
		      if(fgIWPixmap != null)
		      {
		        var bm:GBitmap= getBitmap(rect, subsample, 1, null);
		
		        if((bm != null) && (pm != null))
		        {
		          var fgPixmap:GPixmap= fgPixmap;
		          var w:int= fgPixmap.columns();
		          var h:int= fgPixmap.rows();
		          var red:int= compute_red(width, height, w, h);
		
		//          if((red < 1) || (red > 12))
		          if((red < 1) || (red > 16))
		          {
		            return false;
		          }
		//
		//          int supersample = (red <= subsample)
		//            ? 1
		//            : (red / subsample);
		//          int wantedred = supersample * subsample;
		//
		//          if(red == wantedred)
		//          {
		//            pm.stencil(bm, fgPixmap, supersample, rect, gamma_correction);
		//
		//            return 1;
		//          }
		          pm.stencil(bm, fgPixmap, red, subsample, rect, gamma_correction);
		          return true;
		        }
		      }
		    }
		
		    return false;
		  }

			   /**
			   * Compute the subsample rate used.
			   *
			   * @param w full size width
			   * @param h fill size height
			   * @param rw reduced width
			   * @param rh reduced height
			   *
			   * @return subsample rate, or 16
			   */
			  public static function compute_red(
			    w:int,
			    h:int,
			    rw:int,
			    rh:int):int{
			    for(var red:int= 1; red < 16; red++)
			    {
			      if(((((w + red) - 1) / red) == rw) && ((((h + red) - 1) / red) == rh))
			      {
			        return red;
			      }
			    }
			
			    return 16;
  			}
  			
			  /**
			   * Get the foreground bitmap.
			   *
			   * @param rect area of interest
			   * @param subsample rate to subsample
			   * @param align number of alignment pixels
			   * @param retval an old image to fill rather than creating a new image
			   *
			   * @return the bitmap of interest
			   */
			  public final function getBitmap(
			    rect:GRect,
			    subsample:int,
			    align:int,
			    retval:GBitmap):GBitmap{
			    return get_bitmap(rect, subsample, align, null);
			  }
			  
			   /**
			   * Get a bitmap for the specifed region
			   *
			   * @param rect area of interest
			   * @param subsample subsample rate
			   * @param align border alignment
			   * @param components a list of components
			   *
			   * @return the newly created image
			   */
			  public function get_bitmap(
			    rect:GRect,
			    subsample:int,
			    align:int,
			    components:Vector):GBitmap{
			    if(rect.isEmpty())
			    {
			      return new GBitmap();
			    }
			
			    var info:djINFO = djInfo;
			
			    if(info != null)
			    {
			      var width:int= info.width;
			      var height:int= info.height;
			
			      var fgJb2:JB2Image= fgJb2;
			
			      if(
			        (width != 0)
			        && (height != 0)
			        && (fgJb2 != null)
			        && (fgJb2.width == width)
			        && (fgJb2.height == height))
			      {
			        return fgJb2.get_bitmap(rect, subsample, align, 0, components);
			      }
			    }
			
			    return null;
			  }
			  
			  public function set target(target:Sprite):void {
			  	targetSprite = target;
			  }
	}
}